using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Moq;

namespace Catchbase.TestTools
{
    public class Dependencies<T>
    {
        private readonly Lazy<Dictionary<Type, Mock>> _mocks;
        private readonly Lazy<Dictionary<Type, Object>> _objects;

        public Dependencies()
        {
            IEnumerable<Type> types = typeof (T).CtorParams().ToList();
            _mocks = new Lazy<Dictionary<Type, Mock>>(() => CreateMocks(types));
            _objects = new Lazy<Dictionary<Type, object>>(() => CreateInjections(types));
        }

        public IEnumerable<Object> Objects
        {
            get { return _objects.Value.Select(d => GetValidatedObject(d.Value, d.Key)); }
        }

        public Mock<TObject> GetMock<TObject>() where TObject : class
        {
            return (Mock<TObject>) Mocks[typeof (TObject)];
        }

        public void SetFuncInjection<TOf>(TOf injection)
        {
            _objects.Value[typeof(Func<TOf>)] = new Func<TOf>(() => injection);
        }

        private Object GetValidatedObject(Object obj, Type type)
        {
            if(obj == null && IsFuncOfT(type))
            {
                const string missingFuncSetText =
                    "Ojbect is not set for {0}. Have you remebered the funcy factory in TestInitialize?:\n" +
                    "protected override void InitializeTest()\n" +
                    "{{\n" +
                    "    FuncMock<{1}>();\n" +
                    "}}\n";
                Debug.Fail(string.Format(missingFuncSetText, type.Name, type.GetGenericArguments()[0].Name));
            }
            Debug.Assert(obj != null, string.Format("{0} could not be mocked.", NormalizeType(type).Name));
            return obj;
        }

        private Dictionary<Type, Mock> Mocks
        {
            get { return _mocks.Value; }
        }

        private Dictionary<Type, object> CreateInjections(IEnumerable<Type> types)
        {
            return types.ToDictionary(t => t, FindNormalObject);
        }

        private static Dictionary<Type, Mock> CreateMocks(IEnumerable<Type> types)
        {
            return types.Select(NormalizeType).ToDictionary(t => t, CreateMock);
        }

        private static Mock CreateMock(Type type)
        {
            return (Mock) typeof (Mock<>).MakeGenericType(type).CreateInstance();
        }

        private Object FindNormalObject(Type type)
        {
            return IsFuncOfT(type) ? null : Mocks[type].Object;
        }

        private static Type NormalizeType(Type type)
        {
            return IsFuncOfT(type) ? type.GetGenericArguments()[0] : type;
        }

        private static bool IsFuncOfT(Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof (Func<>);
        }
    }
}